package paulscode.sound;

import java.lang.Boolean;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Locale;
import java.util.ListIterator;
import java.util.LinkedList;

/**
 * The SoundSystemConfig class is used to access global sound system settings,
 * and to link with external pluggins. All members of this class are static.
 * SoundSystemConfig is sort of a "catch all" configuration class, so if you are
 * not sure where to find something in the SoundSystem library, this is probably
 * a good place to start. <br>
 * <br>
 * <b><i> SoundSystem License:</b></i><br>
 * <b><br>
 * You are free to use this library for any purpose, commercial or otherwise.
 * You may modify this library or source code, and distribute it any way you
 * like, provided the following conditions are met: <br>
 * 1) You may not falsely claim to be the author of this library or any
 * unmodified portion of it. <br>
 * 2) You may not copyright this library or a modified version of it and then
 * sue me for copyright infringement. <br>
 * 3) If you modify the source code, you must clearly document the changes made
 * before redistributing the modified source code, so other users know it is not
 * the original code. <br>
 * 4) You are not required to give me credit for this library in any derived
 * work, but if you do, you must also mention my website:
 * http://www.paulscode.com <br>
 * 5) I the author will not be responsible for any damages (physical, financial,
 * or otherwise) caused by the use if this library or any part of it. <br>
 * 6) I the author do not guarantee, warrant, or make any representations,
 * either expressed or implied, regarding the use of this library or any part of
 * it. <br>
 * <br>
 * Author: Paul Lamb <br>
 * http://www.paulscode.com </b>
 */
public class SoundSystemConfig {
	// GLOBAL THREAD SYNCHRONIZATION
	/**
	 * Lock object used to synchronize the three threads used by SoundSystem.
	 * Synchronize on this anytime you manually manipulate a Source's
	 * properties.
	 */
	public static final Object THREAD_SYNC = new Object();
	// END GLOBAL THREAD SYNCHRONIZATION

	// GLOBAL IDENTIFIERS

	/**
	 * A normal (non-streaming) source. Also used to define a Channel type as
	 * normal.
	 */
	public static final int TYPE_NORMAL = 0;
	/**
	 * A streaming source. Also used to define a Channel type as streaming.
	 */
	public static final int TYPE_STREAMING = 1;

	/**
	 * Global identifier for no attenuation. Attenuation is how a source's
	 * volume fades with distance. When there is no attenuation, a source's
	 * volume remains constaint regardles of distance.
	 */
	public static final int ATTENUATION_NONE = 0; // no attenuation
	/**
	 * Global identifier for rolloff attenuation. Rolloff attenuation is a
	 * realistic attenuation model, which uses a rolloff factor to determine how
	 * quickly a source fades with distance. A smaller rolloff factor will fade
	 * at a further distance, and a rolloff factor of 0 will never fade. NOTE:
	 * In OpenAL, rolloff attenuation only works for monotone sounds.
	 */
	public static final int ATTENUATION_ROLLOFF = 1; // logrithmic attenuation
	/**
	 * Global identifier for linear attenuation. Linear attenuation is less
	 * realistic than rolloff attenuation, but it allows the user to specify a
	 * maximum "fade distance" where a source's volume becomes zero.
	 */
	public static final int ATTENUATION_LINEAR = 2; // linear attenuation

	/**
	 * A Regular expression for determining if a file's extension is MIDI.
	 */
	public static String EXTENSION_MIDI = ".*[mM][iI][dD][iI]?$";

	/**
	 * A Regular expression for determining if a path is an online URL.
	 */
	public static String PREFIX_URL = "^[hH][tT][tT][pP]://.*";

	// END GLOBAL IDENTIFIERS

	// PRIVATE STATIC VARIABLES

	/**
	 * Handle to the message logger. The default logger can be changed by
	 * overridding the {@link paulscode.sound.SoundSystemLogger
	 * SoundSystemLogger} class and calling the setLogger() method (must be done
	 * BEFORE instantiating the SoundSystem class!)
	 */
	private static SoundSystemLogger logger = null;

	/**
	 * List of library types in their order of priority.
	 */
	private static LinkedList<Class> libraries;

	/**
	 * List of codecs and the file formats they are associated with.
	 */
	private static LinkedList<Codec> codecs = null;

	/**
	 * List of stream listeners.
	 */
	private static LinkedList<IStreamListener> streamListeners = null;
	/**
	 * For synchronizing access to the streamListeners list.
	 */
	private static final Object streamListenersLock = new Object();

	/**
	 * Maximum number of normal (non-streaming) channels that can be created.
	 * NOTE: JavaSound may require the total number of channels (non-streaming +
	 * streaming) to be 32.
	 */
	private static int numberNormalChannels = 28;
	/**
	 * Maximum number of streaming channels that can be created. NOTE: JavaSound
	 * may require the total number of channels (non-streaming + streaming) to
	 * be 32.
	 */
	private static int numberStreamingChannels = 4;
	/**
	 * Overall volume, affecting all sources. Float value (0.0f - 1.0f).
	 */
	private static float masterGain = 1.0f;
	/**
	 * Attenuation model to use if not specified. Attenuation is how a source's
	 * volume fades with distance.
	 */
	private static int defaultAttenuationModel = ATTENUATION_ROLLOFF;
	/**
	 * Default value to use for the rolloff factor if not specified.
	 */
	private static float defaultRolloffFactor = 0.03f;
	/**
	 * Value to use for the doppler factor, for determining Doppler scale.
	 */
	private static float dopplerFactor = 0.0f;
	/**
	 * Value to use for the doppler velocity.
	 */
	private static float dopplerVelocity = 1.0f;
	/**
	 * Default value to use for fade distance if not specified.
	 */
	private static float defaultFadeDistance = 1000.0f;
	/**
	 * Package where the sound files are located (must be followed by '/').
	 */
	private static String soundFilesPackage = "Sounds/";

	/**
	 * Number of bytes to load at a time when streaming.
	 */
	private static int streamingBufferSize = 131072;
	/**
	 * Number of buffers used for each streaming sorce. Slow codecs may require
	 * this number to be greater than 2 to prevent audio skipping during
	 * playback.
	 */
	private static int numberStreamingBuffers = 3;
	/**
	 * The maximum number of bytes to read in for (non-streaming) files.
	 * Increase this value if non-streaming sounds are getting cut off. Decrease
	 * this value if large sound files are causing lag during load time.
	 */
	private static int maxFileSize = 268435456;
	/**
	 * Size of each chunk to read at a time for loading (non-streaming) files.
	 * Increase if loading sound files is causing significant lag.
	 */
	private static int fileChunkSize = 1048576;

	/**
	 * Indicates whether or not there is a codec for reading from MIDI files. If
	 * there is no codec for MIDI, then SoundSystem uses javax.sound.midi.
	 */
	private static boolean midiCodec = false;

	/**
	 * MIDI device to try using as the Synthesizer. May be the full name or part
	 * of the name. If this String is empty, the default Synthesizer will be
	 * used, or one of the common alternate synthesizers if the default
	 * Synthesizer is unavailable.
	 */
	private static String overrideMIDISynthesizer = "";

	// END PRIVATE STATIC VARIABLES

	// THESE TWO METHODS PROVIDE INFORMATION ABOUT THE INDIVIDUAL SOUND
	// LIBRARIES

	/**
	 * Adds an entry to the list of library types. This method has no effect if
	 * the specified library type is already in the list of libraries. NOTE: The
	 * parameterless constructor of the SoundSystem class will try to load
	 * libraries in the order that they were entered into the list.
	 * 
	 * @param libraryClass
	 *            Derivitive of class 'Library'.
	 */
	public static void addLibrary(Class libraryClass)
			throws SoundSystemException {
		if (libraryClass == null)
			throw new SoundSystemException(
					"Parameter null in method 'addLibrary'",
					SoundSystemException.NULL_PARAMETER);
		if (!Library.class.isAssignableFrom(libraryClass))
			throw new SoundSystemException("The specified class does not "
					+ "extend class 'Library' in method 'addLibrary'");

		if (libraries == null)
			libraries = new LinkedList<Class>();

		if (!libraries.contains(libraryClass))
			libraries.add(libraryClass);
	}

	/**
	 * Removes the specified library from the list of library types.
	 * 
	 * @param libraryClass
	 *            Derivitive of class 'Library'.
	 */
	public static void removeLibrary(Class libraryClass)
			throws SoundSystemException {
		if (libraries == null || libraryClass == null)
			return;

		libraries.remove(libraryClass);
	}

	/**
	 * Returns the list of library types.
	 * 
	 * @return LinkedList of classes derived from 'Library', or null if none
	 *         were specified.
	 */
	public static LinkedList<Class> getLibraries() {
		return libraries;
	}

	/**
	 * Checks if the specified library class is compatible on the user's
	 * machine.
	 * 
	 * @param libraryClass
	 *            Library type to check.
	 * @return True or false.
	 */
	public static boolean libraryCompatible(Class libraryClass) {
		if (libraryClass == null) {
			errorMessage("Parameter 'libraryClass' null in method"
					+ "'librayCompatible'");
			return false;
		}
		if (!Library.class.isAssignableFrom(libraryClass)) {
			errorMessage("The specified class does not extend class "
					+ "'Library' in method 'libraryCompatible'");
			return false;
		}

		Object o = runMethod(libraryClass, "libraryCompatible", new Class[0],
				new Object[0]);

		if (o == null) {
			errorMessage("Method 'Library.libraryCompatible' returned "
					+ "'null' in method 'libraryCompatible'");
			return false;
		}

		return (((Boolean) o).booleanValue());
	}

	/**
	 * Return the short title of the specified library, or null if error.
	 * 
	 * @param libraryClass
	 *            Derivitive of class 'Library'.
	 * @return String containing the library title.
	 */
	public static String getLibraryTitle(Class libraryClass) {
		if (libraryClass == null) {
			errorMessage("Parameter 'libraryClass' null in method"
					+ "'getLibrayTitle'");
			return null;
		}
		if (!Library.class.isAssignableFrom(libraryClass)) {
			errorMessage("The specified class does not extend class "
					+ "'Library' in method 'getLibraryTitle'");
			return null;
		}

		Object o = runMethod(libraryClass, "getTitle", new Class[0],
				new Object[0]);
		if (o == null) {
			errorMessage("Method 'Library.getTitle' returned "
					+ "'null' in method 'getLibraryTitle'");
			return null;
		}

		return ((String) o);
	}

	/**
	 * Return the longer description of the specified library, or null if error.
	 * 
	 * @param libraryClass
	 *            Derivitive of class 'Library'.
	 * @return String containing the library title.
	 */
	public static String getLibraryDescription(Class libraryClass) {
		if (libraryClass == null) {
			errorMessage("Parameter 'libraryClass' null in method"
					+ "'getLibrayDescription'");
			return null;
		}
		if (!Library.class.isAssignableFrom(libraryClass)) {
			errorMessage("The specified class does not extend class "
					+ "'Library' in method 'getLibraryDescription'");
			return null;
		}

		Object o = runMethod(libraryClass, "getDescription", new Class[0],
				new Object[0]);
		if (o == null) {
			errorMessage("Method 'Library.getDescription' returned "
					+ "'null' in method 'getLibraryDescription'");
			return null;
		}

		return ((String) o);
	}

	/**
	 * Return whether or not requires reversal of audio data byte-order.
	 * 
	 * @param libraryClass
	 *            Derivitive of class 'Library'.
	 * @return True if byte-order reversal is required.
	 */
	public static boolean reverseByteOrder(Class libraryClass) {
		if (libraryClass == null) {
			errorMessage("Parameter 'libraryClass' null in method"
					+ "'reverseByteOrder'");
			return false;
		}
		if (!Library.class.isAssignableFrom(libraryClass)) {
			errorMessage("The specified class does not extend class "
					+ "'Library' in method 'reverseByteOrder'");
			return false;
		}

		Object o = runMethod(libraryClass, "reversByteOrder", new Class[0],
				new Object[0]);
		if (o == null) {
			errorMessage("Method 'Library.reverseByteOrder' returned "
					+ "'null' in method 'getLibraryDescription'");
			return false;
		}

		return (((Boolean) o).booleanValue());
	}

	// END LIBRARY INFORMATION

	// Use the following methods to interface the private variables above:

	// STATIC NONSYNCHRONIZED INTERFACE METHODS
	/**
	 * Changes the message logger to use for handling status messages, warnings,
	 * and error messages. This method should only be called BEFORE
	 * instantiating the SoundSystem class! If this method is called after the
	 * SoundSystem has been created, there will be handles floating around to
	 * two different loggers, and the results will be undesirable. This method
	 * can be used to change how messages are handled. First, the
	 * {@link paulscode.sound.SoundSystemLogger SoundSystemLogger} class should
	 * be extended and methods overriden to change how messages are handled.
	 * Then, the overridden class should be instantiated, and a call made to
	 * SoundSystemConfig.setLogger() before creating the SoundSystem object. If
	 * an alternate logger is not set by the user before the SoundSystem is
	 * instantiated, then an instance of the base SoundSystemLogger class will
	 * be used by default.
	 * 
	 * @param l
	 *            Handle to a message logger.
	 */
	public static void setLogger(SoundSystemLogger l) {
		logger = l;
	}

	/**
	 * Returns a handle to the message logger.
	 * 
	 * @return The current message logger.
	 */
	public static SoundSystemLogger getLogger() {
		return logger;
	}

	// STATIC SYNCHRONIZED INTERFACE METHODS

	/**
	 * Sets the maximum number of normal (non-streaming) channels that can be
	 * created. Streaming channels are created first, so the higher the maximum
	 * number of streaming channels is set, the fewer non-streaming channels
	 * will be available. If unable to create the number of channels specified,
	 * SoundSystem will create as many as possible. NOTE: Some sound library
	 * pluggins may require the total number of channels (non-streaming +
	 * streaming) to be 32.
	 * 
	 * @param number
	 *            How many normal audio channels.
	 */
	public static synchronized void setNumberNormalChannels(int number) {
		numberNormalChannels = number;
	}

	/**
	 * Returns the maximum number of normal (non-streaming) channels that can be
	 * created.
	 * 
	 * @return Maximum non-streaming channels.
	 */
	public static synchronized int getNumberNormalChannels() {
		return numberNormalChannels;
	}

	/**
	 * Sets the maximum number of streaming channels that can be created.
	 * Streaming channels are created first, so the higher the maximum number of
	 * streaming channels is set, the fewer non-streaming channels will be
	 * available. If unable to create the number of channels specified,
	 * SoundSystem will create as many as possible. NOTE: Some sound library
	 * pluggins may require the total number of channels (non-streaming +
	 * streaming) to be 32.
	 * 
	 * @param number
	 *            How many streaming audio channels.
	 */
	public static synchronized void setNumberStreamingChannels(int number) {
		numberStreamingChannels = number;
	}

	/**
	 * Returns the maximum number of streaming channels that can be created.
	 * 
	 * @return Maximum streaming channels.
	 */
	public static synchronized int getNumberStreamingChannels() {
		return numberStreamingChannels;
	}

	/**
	 * Sets the varriable used for overall volume, affecting all sources.
	 * 
	 * @param value
	 *            Float value (0.0f - 1.0f).
	 */
	public static synchronized void setMasterGain(float value) {
		masterGain = value;
	}

	/**
	 * Returns the value for the overall volume.
	 * 
	 * @return A float value (0.0f - 1.0f).
	 */
	public static synchronized float getMasterGain() {
		return masterGain;
	}

	/**
	 * Sets the default attenuation model to use when one is not specified.
	 * Attenuation is how a source's volume fades with distance.
	 * 
	 * @param model
	 *            A global attenuation model identifier.
	 */
	public static synchronized void setDefaultAttenuation(int model) {
		defaultAttenuationModel = model;
	}

	/**
	 * Returns the default attenuation model used when one is not specified.
	 * 
	 * @return A global attenuation model identifier
	 */
	public static synchronized int getDefaultAttenuation() {
		return defaultAttenuationModel;
	}

	/**
	 * Sets the default rolloff factor to use when one is not specified.
	 * 
	 * @param rolloff
	 *            Rolloff factor.
	 */
	public static synchronized void setDefaultRolloff(float rolloff) {
		defaultRolloffFactor = rolloff;
	}

	/**
	 * Returns the doppler factor, for determining Doppler Effect scale.
	 * 
	 * @return Doppler factor
	 */
	public static synchronized float getDopplerFactor() {
		return dopplerFactor;
	}

	/**
	 * Sets the doppler factor, for determining Doppler Effect scale. Use this
	 * method BEFORE instantiating the SoundSystem. To change the Doppler factor
	 * after the SoundSystem is instantiated, use the
	 * SoundSystem.changeDopplerFactor method instead.
	 * 
	 * @param factor
	 *            Doppler factor.
	 */
	public static synchronized void setDopplerFactor(float factor) {
		dopplerFactor = factor;
	}

	/**
	 * Returns the Doppler Velocity, for use in Doppler Effect.
	 * 
	 * @return Doppler velocity.
	 */
	public static synchronized float getDopplerVelocity() {
		return dopplerVelocity;
	}

	/**
	 * Sets the Doppler velocity, for use in Doppler Effect. Use this method
	 * BEFORE instantiating the SoundSystem. To change the Doppler velocity
	 * after the SoundSystem is instantiated, use the
	 * SoundSystem.changeDopplerVelocity method instead.
	 * 
	 * @param velocity
	 *            Doppler velocity.
	 */
	public static synchronized void setDopplerVelocity(float velocity) {
		dopplerVelocity = velocity;
	}

	/**
	 * Returns the default rolloff factor used when one is not specified.
	 * 
	 * @return Default rolloff factor
	 */
	public static synchronized float getDefaultRolloff() {
		return defaultRolloffFactor;
	}

	/**
	 * Sets the default fade distance to use when one is not specified.
	 * 
	 * @param distance
	 *            Fade Distance.
	 */
	public static synchronized void setDefaultFadeDistance(float distance) {
		defaultFadeDistance = distance;
	}

	/**
	 * Returns the default fade distance used when one is not specified.
	 * 
	 * @return Default fade distance
	 */
	public static synchronized float getDefaultFadeDistance() {
		return defaultFadeDistance;
	}

	/**
	 * Sets the package where sound files are located.
	 * 
	 * @param location
	 *            Path to the sound files location (must be followed by '/').
	 */
	public static synchronized void setSoundFilesPackage(String location) {
		soundFilesPackage = location;
	}

	/**
	 * Returns the package where sound files are located.
	 * 
	 * @return Path to the sound files location
	 */
	public static synchronized String getSoundFilesPackage() {
		return soundFilesPackage;
	}

	/**
	 * Sets the number of bytes to load at a time when streaming.
	 * 
	 * @param size
	 *            Size in bytes.
	 */
	public static synchronized void setStreamingBufferSize(int size) {
		streamingBufferSize = size;
	}

	/**
	 * Returns the number of bytes to load at a time when streaming.
	 * 
	 * @return Size in bytes.
	 */
	public static synchronized int getStreamingBufferSize() {
		return streamingBufferSize;
	}

	/**
	 * Sets the number of buffers used for each streaming sorce. Slow codecs may
	 * require this number to be greater than 2 to prevent audio skipping during
	 * playback.
	 * 
	 * @param num
	 *            How many buffers.
	 */
	public static synchronized void setNumberStreamingBuffers(int num) {
		numberStreamingBuffers = num;
	}

	/**
	 * Returns the number of buffers used for each streaming sorce.
	 * 
	 * @return How many buffers.
	 */
	public static synchronized int getNumberStreamingBuffers() {
		return numberStreamingBuffers;
	}

	/**
	 * Sets the maximum number of bytes to read in for (non-streaming) files.
	 * Increase this value if non-streaming sounds are getting cut off. Decrease
	 * this value if large sound files are causing lag during load time.
	 * 
	 * @param size
	 *            Size in bytes.
	 */
	public static synchronized void setMaxFileSize(int size) {
		maxFileSize = size;
	}

	/**
	 * Returns the maximum number of bytes to read in for (non-streaming) files.
	 * 
	 * @return Size in bytes.
	 */
	public static synchronized int getMaxFileSize() {
		return maxFileSize;
	}

	/**
	 * Sets the size of each chunk to read at a time for loading (non-streaming)
	 * files. Increase if loading sound files is causing significant lag.
	 * 
	 * @param size
	 *            Size in bytes.
	 */
	public static synchronized void setFileChunkSize(int size) {
		fileChunkSize = size;
	}

	/**
	 * Returns the size of each chunk to read at a time for loading
	 * (non-streaming) files.
	 * 
	 * @return Size in bytes.
	 */
	public static synchronized int getFileChunkSize() {
		return fileChunkSize;
	}

	/**
	 * Returns the name of the MIDI synthesizer to use instead of the default,
	 * or empty string if none was specified.
	 * 
	 * @return All or part of a MIDI device name, or empty string for not
	 *         specified.
	 */
	public static synchronized String getOverrideMIDISynthesizer() {
		return overrideMIDISynthesizer;
	}

	/**
	 * Sets the name of the MIDI synthesizer to use instead of the default. If
	 * 'name' is an empty string, the default Synthesizer will be used, or one
	 * of the common alternate synthesizers if the default Synthesizer is
	 * unavailable.
	 * 
	 * @param name
	 *            All or part of the MIDI device name.
	 */
	public static synchronized void setOverrideMIDISynthesizer(String name) {
		overrideMIDISynthesizer = name;
	}

	/**
	 * Uses the specified file extension to associate a particular file format
	 * with the codec used to read audio data from it.
	 * 
	 * @param extension
	 *            File extension to be associated with the specified codec.
	 * @param iCodecClass
	 *            Codec type to use for files with the specified extension.
	 */
	public static synchronized void setCodec(String extension, Class iCodecClass)
			throws SoundSystemException {
		if (extension == null)
			throw new SoundSystemException("Parameter 'extension' null in "
					+ "method 'setCodec'.", SoundSystemException.NULL_PARAMETER);
		if (iCodecClass == null)
			throw new SoundSystemException("Parameter 'iCodecClass' null in "
					+ "method 'setCodec'.", SoundSystemException.NULL_PARAMETER);
		if (!ICodec.class.isAssignableFrom(iCodecClass))
			throw new SoundSystemException("The specified class does "
					+ "not implement interface 'ICodec' in method 'setCodec'",
					SoundSystemException.CLASS_TYPE_MISMATCH);

		if (codecs == null)
			codecs = new LinkedList<Codec>();

		ListIterator<Codec> i = codecs.listIterator();
		Codec codec;

		while (i.hasNext()) {
			codec = i.next();
			if (extension.matches(codec.extensionRegX))
				i.remove();
		}
		codecs.add(new Codec(extension, iCodecClass));

		// Let SoundSystem know if this is a MIDI codec, so it won't use
		// javax.sound.midi anymore:
		if (extension.matches(EXTENSION_MIDI))
			midiCodec = true;
	}

	/**
	 * Returns the codec that can be used to read audio data from the specified
	 * file.
	 * 
	 * @param filename
	 *            File to get a codec for.
	 * @return Codec to use for reading audio data.
	 */
	public static synchronized ICodec getCodec(String filename) {
		if (codecs == null)
			return null;

		ListIterator<Codec> i = codecs.listIterator();
		Codec codec;

		while (i.hasNext()) {
			codec = i.next();
			if (filename.matches(codec.extensionRegX))
				return codec.getInstance();
		}

		return null;
	}

	/**
	 * Indicates whether or not there is a codec for reading from MIDI files. If
	 * there is no codec for MIDI, then SoundSystem uses javax.sound.midi.
	 * 
	 * @return True if there the user defined a MIDI codec.
	 */
	public static boolean midiCodec() {
		return midiCodec;
	}

	/**
	 * Adds an entry to the list of stream listeners. If the instance is already
	 * in the list, the command is ignored.
	 * 
	 * @param streamListener
	 *            Implementation of interface 'IStreamListener'.
	 */
	public static void addStreamListener(IStreamListener streamListener) {
		synchronized (streamListenersLock) {
			if (streamListeners == null)
				streamListeners = new LinkedList<IStreamListener>();

			if (!streamListeners.contains(streamListener))
				streamListeners.add(streamListener);
		}
	}

	/**
	 * Removes an entry from the list of stream listeners.
	 * 
	 * @param streamListener
	 *            Implementation of interface 'IStreamListener'.
	 */
	public static void removeStreamListener(IStreamListener streamListener) {

		synchronized (streamListenersLock) {
			if (streamListeners == null)
				streamListeners = new LinkedList<IStreamListener>();

			if (streamListeners.contains(streamListener))
				streamListeners.remove(streamListener);
		}
	}

	/**
	 * Notifies all stream listeners that an End Of Stream was reached. If there
	 * are no listeners, the command is ignored.
	 * 
	 * @param sourcename
	 *            String identifier of the source which reached the EOS.
	 * @param queueSize
	 *            Number of items left the the stream's play queue, or zero if
	 *            none.
	 */
	public static void notifyEOS(String sourcename, int queueSize) {
		synchronized (streamListenersLock) {
			if (streamListeners == null)
				return;
		}
		final String srcName = sourcename;
		final int qSize = queueSize;

		new Thread() {
			@Override
			public void run() {
				synchronized (streamListenersLock) {
					if (streamListeners == null)
						return;
					ListIterator<IStreamListener> i = streamListeners
							.listIterator();
					IStreamListener streamListener;
					while (i.hasNext()) {
						streamListener = i.next();
						if (streamListener == null)
							i.remove();
						else
							streamListener.endOfStream(srcName, qSize);
					}
				}
			}
		}.start();
	}

	// END STATIC SYNCHRONIZED INTERFACE METHODS

	// PRIVATE INTERNAL METHODS

	/**
	 * Display the specified error message using the current logger.
	 * 
	 * @param message
	 *            Error message to display.
	 */
	private static void errorMessage(String message) {
		if (logger != null)
			logger.errorMessage("SoundSystemConfig", message, 0);
	}

	// We don't know what Class parameter 'c' is, so we will ignore the
	// warning message "unchecked call to getMethod".
	@SuppressWarnings("unchecked")
	/**
	 * Returns the results of calling the specified method from the specified 
	 * class using the specified parameters.
	 * @param c Class to call the method on.
	 * @param method Name of the method.
	 * @param paramTypes Data types of the parameters being passed to the method.
	 * @param params Actual parameters to pass to the method.
	 * @return Specified method's return value, or null if error or void.
	 */
	private static Object runMethod(Class c, String method, Class[] paramTypes,
			Object[] params) {
		Method m = null;
		try {
			m = c.getMethod(method, paramTypes); // <--- generates a warning
		} catch (NoSuchMethodException nsme) {
			errorMessage("NoSuchMethodException thrown when attempting "
					+ "to call method '" + method + "' in "
					+ "method 'runMethod'");
			return null;
		} catch (SecurityException se) {
			errorMessage("Access denied when attempting to call method '"
					+ method + "' in method 'runMethod'");
			return null;
		} catch (NullPointerException npe) {
			errorMessage("NullPointerException thrown when attempting "
					+ "to call method '" + method + "' in "
					+ "method 'runMethod'");
			return null;
		}
		if (m == null) {
			errorMessage("Method '" + method + "' not found for the class "
					+ "specified in method 'runMethod'");
			return null;
		}

		Object o = null;
		try {
			o = m.invoke(null, params);
		} catch (IllegalAccessException iae) {
			errorMessage("IllegalAccessException thrown when attempting "
					+ "to invoke method '" + method + "' in "
					+ "method 'runMethod'");
			return null;
		} catch (IllegalArgumentException iae) {
			errorMessage("IllegalArgumentException thrown when attempting "
					+ "to invoke method '" + method + "' in "
					+ "method 'runMethod'");
			return null;
		} catch (InvocationTargetException ite) {
			errorMessage("InvocationTargetException thrown while attempting "
					+ "to invoke method 'Library.getTitle' in "
					+ "method 'getLibraryTitle'");
			return null;
		} catch (NullPointerException npe) {
			errorMessage("NullPointerException thrown when attempting "
					+ "to invoke method '" + method + "' in "
					+ "method 'runMethod'");
			return null;
		} catch (ExceptionInInitializerError eiie) {
			errorMessage("ExceptionInInitializerError thrown when "
					+ "attempting to invoke method '" + method + "' in "
					+ "method 'runMethod'");
			return null;
		}

		return (o);
	}

	// END PRIVATE INTERNAL METHODS

	// PRIVATE INTERNAL CLASSES

	/**
	 * The Codec class is used to associate individual file formats with the
	 * codecs used to load audio data from them.
	 * 
	 * Author: Paul Lamb
	 */
	private static class Codec {
		/**
		 * A regular expression used to match a file's extension. This is used
		 * to determine the file format.
		 */
		public String extensionRegX;
		/**
		 * Codec used to load audio data from this file format.
		 */
		public Class iCodecClass;

		/**
		 * Constructor: Converts the specified extension string into a regular
		 * expression, and associates that with the specified codec.
		 * 
		 * @param extension
		 *            File extension to be associated with the specified codec.
		 * @param iCodec
		 *            Codec to use for files with the specified extension.
		 */
		public Codec(String extension, Class iCodecClass) {
			extensionRegX = "";
			// Make sure an extension was specified:
			if (extension != null && extension.length() > 0) {
				// We are only interested in the file extension. The filename
				// can begin with whatever:
				extensionRegX = ".*";
				String c;
				for (int x = 0; x < extension.length(); x++) {
					// Each character could be either upper or lower case:
					c = extension.substring(x, x + 1);
					extensionRegX += "[" + c.toLowerCase(Locale.ENGLISH)
							+ c.toUpperCase(Locale.ENGLISH) + "]";
				}
				// The extension will be at the end of the filename:
				extensionRegX += "$";
			}
			// remember the codec to use for this format:
			this.iCodecClass = iCodecClass;
		}

		public ICodec getInstance() {
			if (iCodecClass == null)
				return null;

			Object o = null;
			try {
				o = iCodecClass.newInstance();
			} catch (InstantiationException ie) {
				instantiationErrorMessage();
				return null;
			} catch (IllegalAccessException iae) {
				instantiationErrorMessage();
				return null;
			} catch (ExceptionInInitializerError eiie) {
				instantiationErrorMessage();
				return null;
			} catch (SecurityException se) {
				instantiationErrorMessage();
				return null;
			}

			if (o == null) {
				instantiationErrorMessage();
				return null;
			}

			return (ICodec) o;
		}

		private void instantiationErrorMessage() {
			errorMessage("Unrecognized ICodec implementation in method "
					+ "'getInstance'.  Ensure that the implementing "
					+ "class has one public, parameterless constructor.");
		}
	}
	// END PRIVATE INTERNAL CLASSES
}
